"""
Copyright 2008-2017 VMware, Inc.  All rights reserved. -- VMware Confidential
"""

import os
import re
import signal
import sys
import threading
import time

from functools import partial, wraps
from threading import Thread
from xml.sax.saxutils import escape

import gtk

# Must be imported explicitly otherwise it'll get imported lazily at
# the end after the module is removed from the system.
import gtk.keysyms

import gobject

from vmis.db import db
from vmis.core.common import State
from vmis.core.errors import InstallerExit
from vmis.core.files import LIBDIR
from vmis.core.questions import ValidationError, ValidationErrorNonFatal, \
                                QUESTION_FILE, QUESTION_DIRECTORY, \
                                QUESTION_YESNO, QUESTION_TEXTENTRY, \
                                QUESTION_NUMERIC, QUESTION_CLOSEPROGRAMS, \
                                QUESTION_SHUTDOWNPROGRAM, QUESTION_PORTENTRY, \
                                QUESTION_DUALPORTENTRIES
from vmis.ui import MessageTypes
from vmis.ui.uiAppControl import UIAppControl
from vmis.util.log import getLog
from vmis.util.shell import run
from vmis.util.path import path

from vmis import vmisdebug

VM_SPACING = 6
VM_FRAME_SPACING = VM_SPACING * 2
PROGRESS_UPDATE_THRESHOLD = .005 # Threshold at which we update progress

log = getLog('vmis.ui.gui')

gobject.threads_init()
gtk.gdk.threads_init()

# Set the default Gtk+ window icon here because a MessageDialog may be
# shown before the rest of the UI is setup by the Wizard.
gtk.window_set_default_icon_name(gtk.STOCK_HARDDISK)

def ShowMessage(messageType, message, useWrapper=False):
   """
   Show a message box outside of the context of the main Wizard

   @param messageType: one of MessageType
   @param message: message text to display
   @param useWrapper: Not used, here for compatibility with other UIs
   """
   mapping = { MessageTypes.INFO     : gtk.MESSAGE_INFO,
               MessageTypes.WARNING  : gtk.MESSAGE_WARNING,
               MessageTypes.ERROR    : gtk.MESSAGE_ERROR }

   d = gtk.MessageDialog(type=mapping[messageType], buttons=gtk.BUTTONS_OK,
                         message_format=message)

   # By default the MessageDialog is hidden from the taskbar and pager
   # since it normally has a transient parent.  In this case the main
   # installer window hasn't been created so treat it as being
   # top-level.
   d.set_skip_taskbar_hint(False)
   d.set_skip_pager_hint(False)

   d.set_title('VMware Installer')
   d.run()
   d.destroy()


def dispatchable(func):
   """
   Decorator to call GUI functions.  If we're in the GUI thread,
   go ahead and call the function.  If not, then the function call
   needs to be dispatched to the GUI thread via gobject.idle_add
   """
   @wraps(func)
   def wrapper(*args, **kwargs):
      if threading.currentThread().getName() != 'MainThread':
         return gobject.idle_add(GUIExecFunction, func, args, kwargs)
      else:
         return func(*args, **kwargs)

   return wrapper

def GUIExecFunction(func, args, kwargs):
   """
   Execute a dispatched function in the GUI thread.

   @param func: Function to call
   @param args: Arguments to func
   @param kwargs: Keyword arguments to func
   """
   func(*args, **kwargs)
   # Return False so GTK doesn't try to call this again.
   return False


class ErrorMessage(gtk.HBox):
   """ Error message widget """
   def __init__(self):
      super(ErrorMessage, self).__init__(spacing=VM_SPACING)

      # Set a minimum size for the HBox to avoid automatic resizing
      # when we show or hide the error message
      _, minH = gtk.icon_size_lookup(gtk.ICON_SIZE_BUTTON)
      self.set_size_request(-1, minH)

      self._image = gtk.image_new_from_stock(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_BUTTON)
      self.pack_start(self._image, False)
      self._text = gtk.Label()
      self._text.set_use_markup(True)
      self._text.set_alignment(0, 0.5)
      self._text.set_line_wrap(True)
      self._text.show()
      self.pack_start(self._text, False)
      self.show()

   def Show(self, text):
      """
      Show or hide the error message widget.

      @param text: if non-empty show the error widget with the given
      text, otherwise hide it
      """
      if text:
         self._image.show()
      else:
         self._image.hide()

      self._text.set_markup('<b>' + text + '</b>') # Add bold text.


class BaseBGBox(gtk.HBox):
   """
   An HBox which draws its background using the base color of the current
   state.
   """
   __gsignals__ = { "expose-event": "override" }

   def __init__(self):
      gtk.HBox.__init__(self)

   def do_expose_event(self, event):
      flags = self.flags()
      if (flags & gtk.MAPPED) and (flags & gtk.VISIBLE):
         style = self.get_style()
         gc = style.base_gc[self.state]

         self.window.draw_rectangle(gc, True,
            self.allocation.x, self.allocation.y,
            self.allocation.width, self.allocation.height)

      gtk.HBox.do_expose_event(self, event)


class ScaledImage(gtk.HBox):
   __gsignals__ = { "size-allocate": "override" }

   def __init__(self, pixbuf):
      gtk.HBox.__init__(self)
      self.pixbuf = pixbuf
      self.width = pixbuf.get_width()
      self.height = pixbuf.get_height()
      self.current = None

   def size_request(self):
      if self.current:
         return self.current.size_request()
      else:
         return (self.width, self.height)

   def do_size_allocate(self, allocation):
      self.allocation = allocation
      width = allocation.width
      height = allocation.height

      if (not self.current or
          width  != self.current.allocation.width or
          height != self.current.allocation.height):
         if not self.current:
            self.current = gtk.Image()
            self.current.show()
            self.add(self.current)

         self.current.set_from_pixbuf(self.pixbuf.scale_simple(
            width, height, gtk.gdk.INTERP_NEAREST))

      self.current.size_allocate(allocation)


class BorderImage(gtk.Table):
   """
   A widget which displays an image, and extrapolates the pixels on the border
   of that image on the edges so it resizes attractively.
   """
   def __init__(self, pixbuf):
      super(BorderImage, self).__init__(rows=2, columns=1)

      width = pixbuf.get_width()
      height = pixbuf.get_height()

      image = gtk.Image()
      image.show()
      image.set_from_pixbuf(pixbuf)
      self.attach(image, 0, 1, 0, 1, gtk.FILL, gtk.FILL)

      image = ScaledImage(pixbuf.subpixbuf(0, height - 1, width, 1))
      image.show()
      self.attach(image, 0, 1, 1, 2, gtk.FILL, gtk.FILL | gtk.EXPAND)

class Wizard():
   __gsignals__ = { 'key-press-event': 'override',
                    'delete-event': 'override', }

   def __init__(self, txn):
      """
      @param txn: global transaction object
      """
      self.txn = txn

   def InitialSetup(self):
      # Set up App Control.
      try:
         self.appControl = UIAppControl()
      except:
         self.appControl = None

      self._cancelled = False
      self._lastUpdated = 0     # Last fraction at which progress was updated

      self.window = gtk.Window()
      self.window.get_settings().set_long_property('gtk-xft-dpi', 96 * 1024, '')

      self.window.set_title(u'VMware Installer')
      self.window.set_position(gtk.WIN_POS_CENTER)
      self.window.set_size_request(950, 600)
      self.window.set_default_size(950, 600)
      self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)

      self._error = ErrorMessage()

      bgbox = BaseBGBox()
      bgbox.show()

      header = gtk.HBox(spacing=VM_SPACING)
      header.show()
      bgbox.add(header)
      header.set_border_width(VM_SPACING)

      icon = gtk.Image()
      icon.show()
      icon.set_from_stock(gtk.STOCK_HARDDISK, gtk.ICON_SIZE_DIALOG)
      self._icon = icon

      primaryText, secondaryText = self._getTextBoxes()
      primaryText.show()
      secondaryText.show()
      self._primary = primaryText
      self._secondary = secondaryText

      labelBox = gtk.VBox(spacing=VM_SPACING)
      labelBox.show()
      labelBox.pack_start(primaryText)
      labelBox.pack_start(secondaryText)

      header.pack_start(labelBox, expand=True)
      header.pack_start(icon, expand=False)

      main = gtk.VBox()
      main.show()
      self.window.add(main)
      main.pack_start(bgbox, False)

      h = gtk.HSeparator()
      h.show()
      main.pack_start(h, False)

      h = gtk.HBox()
      h.show()
      main.pack_start(h, True, True)

      self._banner = gtk.EventBox()
      self._banner.show()
      h.pack_start(self._banner, False, False)

      contentBox = gtk.VBox(spacing=VM_SPACING)
      contentBox.show()
      h.pack_start(contentBox, expand=True, fill=True)
      self._content = contentBox
      contentBox.set_border_width(VM_SPACING)

      h = gtk.HSeparator()
      h.show()
      main.pack_start(h, False)

      main.pack_end(self._getButtons(), expand=False)

      # Must be set after button is associated with the Window.
      self._next.grab_default()

      self._progress = gtk.ProgressBar()
      self._progress.show()

      self._primaryProgressMessage = gtk.Label()
      self._primaryProgressMessage.set_alignment(0, 0.5)
      self._primaryProgressMessage.set_use_markup(True)

      self._secondaryProgressMessage = gtk.Label()
      self._secondaryProgressMessage.set_alignment(0, 0.5)
      self._secondaryProgressMessage.set_use_markup(True)

      # Set up event handling for close/cancel events.
      self.window.connect('delete_event', self.do_delete_event)
      self.window.connect('key_press_event', self.do_key_press_event)

   def _getTextBoxes(self):
      """ Construct primary and secondary text boxes """
      primary = gtk.Label()
      primary.set_alignment(-1, 0)
      primary.set_use_markup(True)

      secondary = gtk.Label()
      secondary.set_alignment(-1, 0)
      secondary.set_padding(VM_FRAME_SPACING * 2, -1)
      secondary.set_use_markup(True)

      return (primary, secondary)

   def _handleQuitEvent(self):
      """
      Maps various quit events (like hitting Escape or clicking the
      'x') to their button counterparts.
      """
      if self._cancel.props.sensitive:
         self._cancel.activate()
      elif self._next.props.sensitive:
         self._next.activate()

   def do_key_press_event(self, widget, event):
      if event.keyval == gtk.keysyms.Escape:
         # If the user hits escape try the Cancel button first if it's
         # active.  If it's not then try the Next button (which may be
         # acting as the Close button if Cancel is disabled.)  If
         # neither of them are active then the installer is not in the
         # state to be closing so nothing will happen.
         self._handleQuitEvent()
         return False

   def do_delete_event(self, widget, event):
      self._handleQuitEvent()
      return True

   def _getButtons(self):
      """ Construct buttons """
      box = gtk.HButtonBox()
      box.show()
      box.set_layout(gtk.BUTTONBOX_END)
      box.set_spacing(VM_FRAME_SPACING)
      box.set_border_width(VM_FRAME_SPACING)

      image = gtk.image_new_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
      image.set_padding(VM_SPACING, 0)
      image.set_alignment(-1, 0)

      cancel = gtk.Button(label='_Cancel')
      cancel.show()
      box.pack_start(cancel)
      cancel.set_sensitive(False)
      cancel.set_focus_on_click(True)
      cancel.set_image(image)
      cancel.connect('clicked', self.OnCancelClicked)
      self._cancel = cancel

      image = gtk.image_new_from_stock(gtk.STOCK_GO_BACK, gtk.ICON_SIZE_BUTTON)
      image.set_padding(VM_SPACING, 0)
      image.set_alignment(-1, 0)

      back = gtk.Button(label='_Back')
      back.show()
      box.pack_start(back)
      back.set_sensitive(False)
      back.set_focus_on_click(True)
      back.set_image(image)
      back.connect('clicked', self.OnBackClicked)
      self._back = back

      image = gtk.image_new_from_stock(gtk.STOCK_GO_FORWARD, gtk.ICON_SIZE_BUTTON)
      image.set_padding(VM_SPACING, 0)
      image.set_alignment(-1, 0)

      next = gtk.Button(label='_Next')
      next.show()
      box.pack_start(next)
      next.set_flags(gtk.CAN_DEFAULT)
      next.set_sensitive(False)
      next.set_focus_on_click(True)
      next.set_image(image)
      next.nextClickedAction = self.txn.Next
      next.connect('clicked', self.OnNextClicked)
      next.alreadyClicked = True # Don't allow clicks until it's enabled.
      self._next = next

      return box

   def UserMessage(self, messageType, message, useWrapper=False):
      """
      This will be called from the main installer thread.  We can
      only use GTK objects in the GTK thread, so we need to dispatch
      the call to the GUI thread.  Easy enough, but it would be nice
      to make this a blocking call as well.  Thread magic ensues to
      make a blocking dialog box.
      """
      def DialogCloseSignalHandler(dialog, param1, cond):
         dialog.destroy()
         # Notify the calling thread that the dialog has been destroyed.
         cond.acquire()
         cond.notify()
         cond.release()

      def GTKUserMessage(messageType, message, useWrapper, cond):
         mapping = { MessageTypes.INFO     : gtk.MESSAGE_INFO,
                     MessageTypes.WARNING  : gtk.MESSAGE_WARNING,
                     MessageTypes.ERROR    : gtk.MESSAGE_ERROR }

         d = gtk.MessageDialog(type=mapping[messageType], buttons=gtk.BUTTONS_OK,
                               message_format=message, flags=gtk.DIALOG_MODAL)

         d.set_title('VMware Installer')
         d.connect('response', DialogCloseSignalHandler, cond)
         d.show()

      if threading.currentThread().getName() != 'MainThread':
         cond = threading.Condition()
         gobject.idle_add(GUIExecFunction, GTKUserMessage,
                          (messageType, message, useWrapper, cond), {})

      else:
         log.error('GUI: Calling GTKUserMessage from GUI thread is'
                   ' not allowed!')
         raise NotImplementedError

      # Wait for the dialog to be destroyed.
      cond.acquire()
      cond.wait()
      cond.release()

   @dispatchable
   def ShowPromptInstall(self):
      """ Display the screen before the product will be installed """
      self.ClearContents()

      self._next.grab_focus()

      label = gtk.Label()
      label.show()
      label.set_use_markup(True)
      label.set_markup('<b>The product is ready to be installed.</b>')
      label.set_alignment(0, 0.5)
      self._content.pack_start(label)

      self.window.show()

   @dispatchable
   def SetNextType(self, nextType, args=(), buttonText=None):
      """ Modify the appearance of the next button """
      self._next.nextClickedArgs = args
      if nextType == 'close':
         stock = gtk.STOCK_CLOSE
         text = buttonText if buttonText else '_Close'

         # when nextType is close assume that Back and Cancel should
         # be hidden (like during the Finished state).
         self._back.hide()
         self._cancel.hide()
         # Remap the next button to the correct function.
         self._next.nextClickedAction = self.txn.Next
      elif nextType == 'install':
         stock = gtk.STOCK_JUMP_TO
         text = buttonText if buttonText else '_Install'
         # Remap the install button to the correct function.
         self._next.nextClickedAction = self.txn.Next
      elif nextType == 'next':
         stock = gtk.STOCK_GO_FORWARD
         text = buttonText if buttonText else '_Next'
         # Remap the next button to the correct function
         self._next.nextClickedAction = self.txn.Next
      elif nextType == 'retry':
         stock = gtk.STOCK_GO_FORWARD
         text = buttonText if buttonText else '_Retry'
          # The retry callback function is handled by the UI pane,
          # no need to remap it here.
      else:
         raise ValueError('nextType must be one of close, install, or next')

      image = gtk.image_new_from_stock(stock, gtk.ICON_SIZE_BUTTON)
      image.show()

      self._next.set_image(image)
      self._next.set_label(text)

   def SetBannerImage(self, imagePath):
      """
      Set the side image banner

      @param imagePath: file path to the image to display
      """
      child = self._banner.get_child()
      child and self._banner.remove(child)

      banner = BorderImage(gtk.gdk.pixbuf_new_from_file(imagePath))
      banner.show()
      self._banner.add(banner)

   def SetHeaderImage(self, imagePath):
      """ Set image in header """
      self._icon.set_from_file(imagePath)

   @dispatchable
   def SetTitle(self, title):
      """ Set title for window """
      self.window.set_title(title)

   def SetIconImages(self, filePaths):
      """ Set default window icon images """
      images = [gtk.gdk.pixbuf_new_from_file(image) for image in filePaths]
      gtk.window_set_default_icon_list(*images)

   @dispatchable
   def HideCancel(self):
      """ Hide the cancel button """
      self._cancel.hide()

   @dispatchable
   def EnableCancel(self, enabled):
      """ Enable/disable the cancel button """
      self._cancel.set_sensitive(enabled)

   @dispatchable
   def HideBack(self):
      """ Hide the back button """
      self._back.hide()

   @dispatchable
   def EnableBack(self, enabled):
      """ Enable/disable the back button """
      self._back.set_sensitive(enabled)

   @dispatchable
   def HideNext(self):
      """ Hide the next button """
      self._next.hide()

   @dispatchable
   def EnableNext(self, enabled):
      """ Enable/disable the next button """
      from vmis import vmisdebug
      self._next.set_sensitive(enabled)

   def OnCancelClicked(self, btn):
      """ Callback when cancel is clicked """
      log.debug('Cancel clicked')

      # Theoretically there could be multiple clicked events queued up
      # to run before we have a chance to disable the button.
      if self._cancelled:
         return True

      # We can't let multiple AbortError's be raised, otherwise the
      # main thread may start deadlocking.
      self._canceled = True
      self._cancel.set_sensitive(False)
      self.txn.Abort()

      return True               # Don't allow signal to propogate up

   def OnBackClicked(self, btn):
      """ Callback when back is clicked """
      log.debug('Back clicked')
      self.txn.Back()

   def ResetNext(self):
      """ Resets the next button to be valid. """
      self._next.alreadyClicked = False

   def OnNextClicked(self, btn):
      """ Callback when next is clicked """
      # If the next button has already been processed for this target, ignore
      # subsequent extranneous clicks.
      if self._next.alreadyClicked:
         return
      log.debug('Next clicked')
      self._next.alreadyClicked = True
      self._next.nextClickedAction(*self._next.nextClickedArgs)

   @dispatchable
   def SetPrimaryText(self, text):
      """ Set the primary header text """
      self._primary.set_markup('<b>%s</b>' % text)

   @dispatchable
   def SetSecondaryText(self, text):
      """ Set the secondary header text """
      self._secondary.set_markup(text)

   def ClearContents(self):
      """ Empty the content section of the UI """
      for c in self._content.get_children():
         self._content.remove(c)

   @dispatchable
   def ShowProgress(self):
      """ Display installation progress """
      self.ClearContents()

      # XXX: force progress bar to middle
      l = gtk.Label()
      l.show()
      self._content.pack_start(l)

      self._content.pack_start(self._primaryProgressMessage, False)
      self._primaryProgressMessage.show()

      align = gtk.Alignment()
      align.show()
      align.set_padding(0, 0, 24, 24)
      align.add(self._secondaryProgressMessage)
      self._secondaryProgressMessage.show()
      self._content.pack_start(align, False)
      self._content.pack_start(self._progress, False)

      # XXX: force progress bar to middle
      l2 = gtk.Label()
      l2.show()
      self._content.pack_start(l2)

      self.window.show()

   @dispatchable
   def SetPrimaryProgressMessage(self, text):
      """ Set primary installation text """
      self._primaryProgressMessage.set_markup('<big><b>%s</b></big>' % text)

   @dispatchable
   def SetSecondaryProgressMessage(self, text):
      """ Set secondary installation text """
      self._secondaryProgressMessage.set_markup(text)

   def SetProgress(self, fraction):
      """ Set installation progresss """
      # We have to limit the number of times the progress bar is
      # updated, otherwise things are slowed down considerably.  The
      # GIL should be given up when calling out to Gtk+ so it's not
      # completely apparent where the delay comes from (perhaps purely
      # from the function call overhead.)
      @dispatchable
      def update(progress, fraction):
         progress.set_fraction(fraction)

      # Look at absolute value to account for the case where the
      # progress bar is being decreased, like during rollback.
      delta = abs(fraction - self._lastUpdated)

      if delta > PROGRESS_UPDATE_THRESHOLD:
         update(self._progress, fraction)
         self._lastUpdated = fraction

   @dispatchable
   def ShowFinish(self, success, message):
      """ Display finish page """
      self.ClearContents()

      if success:
         icon = gtk.STOCK_APPLY
      else:
         icon = gtk.STOCK_DIALOG_ERROR

      box = gtk.HBox(spacing=VM_SPACING)
      box.show()
      self._content.pack_start(box)

      image = gtk.Image()
      image.show()
      box.pack_start(image, False)
      image.set_from_stock(icon, gtk.ICON_SIZE_BUTTON)

      label = gtk.Label()
      label.show()
      label.set_line_wrap(True)
      box.pack_start(label)
      label.set_markup('<b>%s</b>' % message)

      self.window.show()
      self._next.grab_focus()

   @dispatchable
   def ShowYesNo(self, question, text, callback, default):
      """
      Display a directory page

      @param text: text to display
      @param callback: validator callback with text entry
      @param default: default entry
      """
      self.ClearContents()

      space = gtk.Label()
      space.show()
      self._content.pack_start(space)
      self._content.pack_start(self._error, expand=False)

      # 1.0 allows text box to expand entirely.
      align = gtk.Alignment(xscale=1.0, yscale=1.0)
      align.show()
      align.set_padding(0, 0, 24, 24)

      box = gtk.HBox(spacing=VM_SPACING)
      box.show()
      align.add(box)
      self._content.pack_start(align, expand=False)

      l = gtk.Label()
      l.set_alignment(0, 0.5)
      l.set_use_markup(True)
      l.set_markup('<b>%s</b>' % text)
      l.set_size_request(726, -1)
      l.set_line_wrap(True)
      l.show()
      box.pack_start(l)

      # 1.0 allows text box to expand entirely.
      align = gtk.Alignment(xscale=1.0, yscale=1.0)
      align.show()
      align.set_padding(0, 0, 24, 24)

      box = gtk.HBox()
      box.show()
      align.add(box)
      self._content.pack_start(align, expand=False)

      rbyes = gtk.RadioButton(label='_Yes')
      rbyes.show()
      rbno = gtk.RadioButton(group=rbyes, label='N_o')
      rbno.show()

      vbox = gtk.VBox()
      vbox.show()
      box.pack_start(vbox, expand=False)

      vbox.pack_start(rbyes, expand=False)
      vbox.pack_start(rbno, expand=False)

      def onChanged(widget):
         if widget.get_active():
           label = widget.get_label()
           label = re.sub('_', '', label) # Remove accelerator underscores
           self.txn.put(partial(callback, label))

      # Must connect before setting text to signal initial validation.
      if default == 'yes':
         rbyes.set_active(True)
      else:
         rbno.set_active(True)
      rbyes.connect('group-changed', onChanged)
      rbyes.connect('toggled', onChanged)
      rbno.connect('toggled', onChanged)

      # HTML link
      def onLinkPushed(widget, _notUsed, args):
         linktext = args[0]
         linkhtml = args[1]

         # Create a window with a bold title at the top, a text
         # box below it full of the supplied text, and a close
         # button on the bottom.
         window = gtk.Window()
         window.set_title('Information')
         window.set_position(gtk.WIN_POS_CENTER)
         window.set_default_size(680, 520)
         window.set_modal(True)
         window.set_transient_for(self.window)

         main = gtk.VBox()
         main.show()
         window.add(main)

         # This is for the header
         vbox = gtk.VBox()
         vbox.show()
         header = gtk.Label('<b>' + linktext + '</b>')
         header.show()
         header.set_use_markup(True)
         spacer = gtk.Label('');  spacer.show()
         spacer2 = gtk.Label(''); spacer2.show()
         vbox.pack_start(spacer)
         vbox.pack_start(header)
         vbox.pack_start(spacer2)

         # This will hold the main text box
         hbox1 = gtk.HBox()
         hbox1.show()

         # This will hold the button
         hbox2 = gtk.HButtonBox()
         hbox2.show()
         hbox2.set_layout(gtk.BUTTONBOX_END)
         hbox2.set_spacing(VM_FRAME_SPACING)
         hbox2.set_border_width(VM_FRAME_SPACING)

         # Pack the main window with our 3 rows.
         main.pack_start(vbox, expand=False)
         main.pack_start(hbox1, expand=True, fill=True)
         main.pack_start(hbox2, expand=False, fill=False)

         # Callback to close the window
         def onCloseClicked(widget, theWindow):
            theWindow.destroy()

         # Add the close button to hbox2
         close = gtk.Button('_Close')
         close.show()
         close.connect('clicked', onCloseClicked, window)
         hbox2.pack_end(close)

         # Create the text buffer and fill it
         buffer = gtk.TextBuffer()
         buffer.set_text(linkhtml)

         # Create the text view
         tview = gtk.TextView()
         tview.show()

         # Create a scrolled box for the text view
         scroll = gtk.ScrolledWindow()
         scroll.show()
         scroll.add(tview)
         scroll.set_shadow_type(gtk.SHADOW_IN)
         scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)

         # Set up the text box
         tview.set_buffer(buffer)
         tview.set_wrap_mode(gtk.WRAP_WORD)
         tview.set_editable(False)

         # And back it all into it's box
         hbox1.pack_start(scroll)

         window.show()

      # If any text was supplied for a link, add one.
      if question.linktext:
         link = gtk.LinkButton('', label=question.linktext)
         link.set_relief(gtk.RELIEF_NONE)
         link.set_use_underline(True)
         link.show()
         # Intercept the default hook to open our own window instead.
         gtk.link_button_set_uri_hook(onLinkPushed, [question.linktext, question.linkhtml])
         vbox.pack_start(link, expand=False)

      space = gtk.Label()
      space.show()
      self._content.pack_end(space, True, True)

      self.window.show()
      self._next.grab_focus()

   @dispatchable
   def ShowTextEntry(self, question, text, callback, default):
      """
      Display a text entry

      @param question: original question object
      @param text: text to display
      @param callback: validator callback with text entry
      @param default: default entry
      """
      self.ClearContents()

      space = gtk.Label()
      space.show()
      self._content.pack_start(space)
      self._content.pack_start(self._error, expand=False)

      # 1.0 allows text box to expand entirely.
      align = gtk.Alignment(xscale=1.0, yscale=1.0)
      align.show()
      align.set_padding(0, 0, 24, 24)

      box = gtk.HBox(spacing=VM_SPACING)
      box.show()
      align.add(box)
      self._content.pack_start(align, expand=False)

      l = gtk.Label()
      l.set_alignment(0, 0.5)
      l.set_use_markup(True)
      l.set_markup('<b>%s</b>' % question.header)
      l.set_size_request(726, -1)
      l.set_line_wrap(True)
      l.show()
      box.pack_start(l)

      # 1.0 allows text box to expand entirely.
      align = gtk.Alignment(xscale=1.0, yscale=1.0)
      align.show()
      align.set_padding(0, 0, 24, 24)

      box = gtk.HBox(spacing=VM_SPACING)
      box.show()
      align.add(box)
      self._content.pack_start(align, expand=False)

      entry = gtk.Entry()
      entry.show()
      box.pack_start(entry)

      def onChanged(widget):
         self.txn.put(partial(callback, entry.get_text()))

      # Must connect before setting text to signal initial validation.
      entry.connect_after('changed', onChanged)
      entry.set_text(default)

      # 1.0 allows text box to expand entirely.
      align = gtk.Alignment(xscale=1.0, yscale=1.0)
      align.show()
      align.set_padding(0, 0, 24, 24)

      box = gtk.HBox(spacing=VM_SPACING)
      box.show()
      align.add(box)
      self._content.pack_start(align, expand=False)

      lf = gtk.Label()
      lf.set_alignment(0, 0.5)
      lf.set_use_markup(True)
      lf.set_markup('%s' % question.footer)
      lf.set_size_request(726, -1)
      lf.set_line_wrap(True)
      lf.show()
      box.pack_start(lf)

      space = gtk.Label()
      space.show()
      self._content.pack_end(space)

      self.window.show()
      self._next.grab_focus()

   @dispatchable
   def ShowFile(self, question, text, callback, filename):
      """
      Display a directory page

      @param text: text to display
      @param callback: validator callback with text entry
      @param filename: default entry
      """
      self.ClearContents()

      space = gtk.Label()
      space.show()
      self._content.pack_start(space)
      self._content.pack_start(self._error, expand=False)

      # 1.0 allows text box to expand entirely.
      align = gtk.Alignment(xscale=1.0, yscale=1.0)
      align.show()
      align.set_padding(0, 0, 24, 24)

      box = gtk.HBox(spacing=VM_SPACING)
      box.show()
      align.add(box)
      self._content.pack_start(align, expand=False)

      l = gtk.Label()
      l.set_alignment(0, 0.5)
      l.set_use_markup(True)
      l.set_markup('<b>%s</b>' % text)
      l.set_size_request(726, -1)
      l.set_line_wrap(True)
      l.show()
      box.pack_start(l)

      # 1.0 allows text box to expand entirely.
      align = gtk.Alignment(xscale=1.0, yscale=1.0)
      align.show()
      align.set_padding(0, 0, 24, 24)

      box = gtk.HBox(spacing=VM_SPACING)
      box.show()
      align.add(box)
      self._content.pack_start(align, expand=False)

      entry = gtk.Entry()
      entry.show()
      box.pack_start(entry)

      def onChanged(widget):
         self.txn.put(partial(callback, entry.get_text()))

      # Must connect before setting text to signal initial validation.
      entry.connect_after('changed', onChanged)
      entry.set_text(filename)

      def onClicked(btn, entry):
         """ Browse button callback """
         # XXX: this makes the main program exit
         d = gtk.FileChooserDialog(title=u'Select File',
                                   action=gtk.FILE_CHOOSER_ACTION_OPEN,
                                   buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
                                            gtk.STOCK_OPEN, gtk.RESPONSE_OK))
         d.set_transient_for(self.window)

         if d.run() == gtk.RESPONSE_OK:
            entry.set_text(d.get_filename())

         d.destroy()

      image = gtk.image_new_from_stock(gtk.STOCK_FILE, gtk.ICON_SIZE_BUTTON)
      image.show()
      image.set_padding(VM_SPACING, 0)
      image.set_alignment(-1, 0)

      browse = gtk.Button(label='B_rowse')
      browse.show()
      box.pack_start(browse, expand=False)
      browse.set_image(image)

      browse.connect('clicked', onClicked, entry)

      space = gtk.Label()
      space.show()
      self._content.pack_end(space)

      self.window.show()
      self._next.grab_focus()

   @dispatchable
   def ShowDirectory(self, question, text, callback, filename):
      """
      Display a directory page

      @param text: text to display
      @param callback: validator callback with text entry
      @param filename: default entry
      """
      self.ClearContents()

      space = gtk.Label()
      space.show()
      self._content.pack_start(space)
      self._content.pack_start(self._error, expand=False)

      # 1.0 allows text box to expand entirely.
      align = gtk.Alignment(xscale=1.0, yscale=1.0)
      align.show()
      align.set_padding(0, 0, 24, 24)

      box = gtk.HBox(spacing=VM_SPACING)
      box.show()
      align.add(box)
      self._content.pack_start(align, expand=False)

      l = gtk.Label()
      l.set_alignment(0, 0.5)
      l.set_use_markup(True)
      l.set_markup('<b>%s</b>' % text)
      l.set_size_request(726, -1)
      l.set_line_wrap(True)
      l.show()
      box.pack_start(l)

      # 1.0 allows text box to expand entirely.
      align = gtk.Alignment(xscale=1.0, yscale=1.0)
      align.show()
      align.set_padding(0, 0, 24, 24)

      box = gtk.HBox(spacing=VM_SPACING)
      box.show()
      align.add(box)
      self._content.pack_start(align, expand=False)

      entry = gtk.Entry()
      entry.show()
      box.pack_start(entry)

      def onChanged(widget):
         self.txn.put(partial(callback, entry.get_text()))

      # Must connect before setting text to signal initial validation.
      entry.connect_after('changed', onChanged)
      entry.set_text(filename)

      def onClicked(btn, entry):
         """ Browse button callback """
         # XXX: this makes the main program exit
         d = gtk.FileChooserDialog(title=u'Select Directory',
                                   action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
                                   buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
                                            gtk.STOCK_OPEN, gtk.RESPONSE_OK))
         d.set_transient_for(self.window)

         if d.run() == gtk.RESPONSE_OK:
            entry.set_text(d.get_filename())

         d.destroy()

      image = gtk.image_new_from_stock(gtk.STOCK_DIRECTORY, gtk.ICON_SIZE_BUTTON)
      image.show()
      image.set_padding(VM_SPACING, 0)
      image.set_alignment(-1, 0)

      browse = gtk.Button(label='B_rowse')
      browse.show()
      box.pack_start(browse, expand=False)
      browse.set_image(image)

      browse.connect('clicked', onClicked, entry)

      space = gtk.Label()
      space.show()
      self._content.pack_end(space)

      self.window.show()
      self._next.grab_focus()

   @dispatchable
   def ShowNumeric(self, question, text, callback, default):
      """
      Display a directory page

      @param text: text to display
      @param callback: validator callback with text entry
      @param default: default entry
      """
      self.ClearContents()

      space = gtk.Label()
      space.show()
      self._content.pack_start(space)
      self._content.pack_start(self._error, expand=False)

      # 1.0 allows text box to expand entirely.
      align = gtk.Alignment(xscale=1.0, yscale=1.0)
      align.show()
      align.set_padding(0, 0, 24, 24)

      box = gtk.HBox(spacing=VM_SPACING)
      box.show()
      align.add(box)
      self._content.pack_start(align, expand=False)

      l = gtk.Label()
      l.set_alignment(0, 0.5)
      l.set_use_markup(True)
      l.set_markup('<b>%s</b>' % text)
      l.set_size_request(726, -1)
      l.set_line_wrap(True)
      l.show()
      box.pack_start(l)

      # 1.0 allows text box to expand entirely.
      align = gtk.Alignment(xscale=1.0, yscale=1.0)
      align.show()
      align.set_padding(0, 0, 24, 24)

      box = gtk.HBox(spacing=VM_SPACING)
      box.show()
      align.add(box)
      self._content.pack_start(align, expand=False)

      entry = gtk.Entry()
      entry.set_width_chars(5)
      entry.show()
      box.pack_start(entry, expand=False, fill=False)

      def onChanged(widget):
         self.txn.put(partial(callback, entry.get_text()))

      # Must connect before setting text to signal initial validation.
      entry.connect_after('changed', onChanged)
      entry.set_text(default)

      space = gtk.Label()
      space.show()
      self._content.pack_end(space)

      self.window.show()
      self._next.grab_focus()

   @dispatchable
   def ShowPortEntry(self, question, text, callback, default):
      """
      Show a port entry.

      @param text: text to display
      @param callback: validator callback with text entry
      @param default: default entry
      """
      self.ClearContents()

      space = gtk.Label()
      space.show()
      self._content.pack_start(space)
      self._content.pack_start(self._error, expand=False)

      align = gtk.Alignment(xscale=1.0, yscale=1.0)
      align.show()
      align.set_padding(0, 0, 24, 24)

      box = gtk.HBox(spacing=VM_SPACING)
      box.show()
      align.add(box)
      self._content.pack_start(align, expand=False)

      l = gtk.Label()
      l.set_alignment(0, 0.5)
      l.set_use_markup(True)
      l.set_markup('<b>%s</b>' % text)
      l.set_size_request(726, -1)
      l.set_line_wrap(True)
      l.show()
      box.pack_start(l)

      align = gtk.Alignment(xscale=1.0, yscale=1.0)
      align.show()
      align.set_padding(0, 0, 24, 24)

      hbox = gtk.HBox(spacing=VM_SPACING)
      hbox.show()
      align.add(hbox)
      self._content.pack_start(align, expand=False, fill=False)

      labelEntry = gtk.Label(question.label)
      labelEntry.show()
      hbox.pack_start(labelEntry, expand=False, fill=False)

      entry = gtk.Entry()
      entry.set_width_chars(5)
      entry.show()
      hbox.pack_start(entry, expand=False, fill=False)

      def onChanged(widget):
         self.txn.put(partial(callback, entry.get_text()))

      # Connect after the default handler to get initial validation
      entry.connect_after('changed', onChanged)

      # Parse our default
      if default:
         entry.set_text(default)

      space = gtk.Label()
      space.show()
      self._content.pack_end(space)

      self.window.show()
      self._next.grab_focus()

   @dispatchable
   def ShowDualPortEntries(self, question, text, callback, default):
      """
      Display two port entries

      @param text: text to display
      @param callback: validator callback with text entry
      @param default: default entry
      """
      self.ClearContents()

      space = gtk.Label()
      space.show()
      self._content.pack_start(space)
      self._content.pack_start(self._error, expand=False)

      # 1.0 allows text box to expand entirely.
      align = gtk.Alignment(xscale=1.0, yscale=1.0)
      align.show()
      align.set_padding(0, 0, 24, 24)

      box = gtk.HBox(spacing=VM_SPACING)
      box.show()
      align.add(box)
      self._content.pack_start(align, expand=False)

      l = gtk.Label()
      l.set_alignment(0, 0.5)
      l.set_use_markup(True)
      l.set_markup('<b>%s</b>' % text)
      l.set_size_request(726, -1)
      l.set_line_wrap(True)
      l.show()
      box.pack_start(l)

      # 1.0 allows text box to expand entirely.
      align = gtk.Alignment(xscale=1.0, yscale=1.0)
      align.show()
      align.set_padding(0, 0, 24, 24)

      hbox = gtk.HBox(spacing=VM_SPACING)
      hbox.show()
      align.add(hbox)
      self._content.pack_start(align, expand=False, fill=False)

      table = gtk.Table(2, 2, homogeneous=False)
      table.show()
      table.set_row_spacings(VM_SPACING)
      table.set_col_spacings(VM_SPACING)
      hbox.pack_start(table, expand=False, fill=False)

      labelEntry = gtk.Label(question.label1)
      labelEntry.show()
      table.attach(labelEntry, 0,1, 0,1, xoptions=0, yoptions=0)
      entry = gtk.Entry()
      entry.set_width_chars(5)
      entry.show()
      table.attach(entry, 1,2, 0,1, xoptions=0, yoptions=0)

      labelEntry1 = gtk.Label(question.label2)
      labelEntry1.show()
      table.attach(labelEntry1, 0,1, 1,2, xoptions=0, yoptions=0)
      entry1 = gtk.Entry()
      entry1.set_width_chars(5)
      entry1.show()
      table.attach(entry1, 1,2, 1,2, xoptions=0, yoptions=0)

      def onChanged(widget):
         # Combine our ports into a single string, which is how we need to store the value.
         str = '%s/%s' % (entry.get_text(), entry1.get_text())
         self.txn.put(partial(callback, str))

      # Must connect before setting text to signal initial validation.
      entry.connect_after('changed', onChanged)
      entry1.connect_after('changed', onChanged)

      # Parse our default
      if default:
         ports = default.split('/');
         if len(ports) == 2:
            entry.set_text(ports[0])
            entry1.set_text(ports[1])

      space = gtk.Label()
      space.show()
      self._content.pack_end(space)

      self.window.show()
      self._next.grab_focus()

   @dispatchable
   def ClosePrograms(self, question, text, callback, default):
      """
      Display a close programs dialog.

      @param question: The question object attached to this call
      @param text: text to display
      @param callback: validator callback with text entry
      @param default: default entry
      """
      # Make sure closeProgramsState is defined as 0 if this is our first
      # use of it.
      try:
         getattr(self, 'closeProgramsState')
      except AttributeError:
         self.closeProgramsState = 0

      appControlSucceeded = False
      try:
         # Load icons and create the list on the fly from our list of items.
         # XXX: These icons are supplied by the vmware-installer component.
         installerDir = path(os.environ['VMWARE_INSTALLER'])
         iconws = gtk.gdk.pixbuf_new_from_file(installerDir/'artwork/vmware-workstation.png')
         iconpl = gtk.gdk.pixbuf_new_from_file(installerDir/'artwork/vmware-player.png')
         iconvm = gtk.gdk.pixbuf_new_from_file(installerDir/'artwork/vmware-vm.png')

         openitems = []

         # Test whether we can use vmware-app-control.  If the app is not executable, this
         # will throw an exception.  Also fetch the VM and App count while we're at it.
         self.appControl.Initialize()

         # If there are any, then run the app again to retrieve the names of said VMs.
         if (self.appControl.numVMs > 0):
            for i in range(self.appControl.numVMs):
               name = self.appControl.GetVMInfo(i)
               openitems.append((iconvm, name))

         if (self.appControl.numApps > 0):
            for i in range(self.appControl.numApps):
               (name, product) = self.appControl.GetAppInfo(i)

               # Choose the correct icon.
               if product == 'workstation':
                  icon = iconws
               else:
                  icon = iconpl
               openitems.append((icon, name))

         # We've successfully gathered all the information we need.
         # Now start to build the GUI.
         self.ClearContents()
         self._content.pack_start(self._error, False)

         # Add the text prompt
         textlabel = gtk.Label()
         textlabel.set_alignment(0, 0.5)
         textlabel.set_line_wrap(True)
         textlabel.set_use_markup(True)
         textlabel.show()
         self._content.pack_start(textlabel, False)

         # Add a list box here.  Using a TreeView containing a ListStore, we'll add
         # 2 CellRenderers (2 columns): A pixbuf (icon), and text containing the name
         # of the VMware UI or VM to close down.
         ls = gtk.ListStore(gtk.gdk.Pixbuf, str)
         tv = gtk.TreeView(model=ls)
         tv.set_headers_visible(False)
         tv.show()

         col = gtk.TreeViewColumn()
         tv.append_column(col)

         # Add the columns and tie to renderers.
         crp = gtk.CellRendererPixbuf()
         crt1 = gtk.CellRendererText()
         col.pack_start(crp, False)
         col.add_attribute(crp, 'pixbuf', 0)
         col.pack_start(crt1, True)
         col.add_attribute(crt1, 'text', 1)

         # Create a scrolled window to house this.
         scroll = gtk.ScrolledWindow()
         scroll.show()
         scroll.add(tv)
         scroll.set_shadow_type(gtk.SHADOW_IN)
         scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
         self._content.pack_start(scroll, True)

         if openitems:
            # If there were any open items, then add them to the list.
            for oi in openitems:
               ls.append(oi)

            self.window.show()
            # Based on our state, we are either displaying this for the first time, or
            # there were UIs and VMs that did not shut down the first time.
            # Display a different message to the user depending on this.
            if self.closeProgramsState == 0:
               textlabel.set_markup('<b>The following virtual machines and VMware applications '
                                    'are running.  Please suspend or close them, or click '
                                    '\'Next\' to do this automatically.</b>')
            else:
               textlabel.set_markup('<b>The following virtual machines and VMware applications '
                                    'could not be shut down automatically.  Please suspend or '
                                    'close them, then click \'Next\' to continue.</b>')

         else:
            # Otherwise things are now good, change our text for the user and enable the Next button.
            textlabel.set_markup('<b>All virtual machines and programs have been suspended or closed.</b>')

         # Create a button to close open VMs.
         box = gtk.HButtonBox()
         box.show()
         box.set_layout(gtk.BUTTONBOX_START)
         self._content.pack_start(box, False)

         # Define appcontrol shutdownall thread callback
         def appControlShutDownAll(pobj, appcontrol):
             appcontrol.ShutdownAll()

         # Define our callback for the "Close Open VMs and Programs" button
         def onSuspend(widget, gui, appControl, question, text, callback, default):

            # Define a new thread for appcontrol to close VMs and VMware applications.
            appControlThread = Thread(args=(self, appControl), target=appControlShutDownAll)

            # Disable Next button
            self.EnableNext(False)

            # Update installer window text label to notify user what's going on
            textlabel.set_markup('<b>Closing VMware VMs and programs ... </b>')
            self.window.show()

            # Start the thread
            appControlThread.start()

            # Wait for appcontrol shutdownall thread to come back
            while appControlThread.isAlive():
                time.sleep(0.05)
                gtk.main_iteration(block=False)
            self.window.show()

            # Run the current state through the validation in questions.py
            self.txn.put(partial(callback, True))
            # And call ClosePrograms again.  Now that everything's closed, we want to refresh
            # the UI.
            self.EnableNext(True)
            # Ensure that the next button can be used again for the next cycle.
            self.txn.ui.ResetNext()
            # Call ClosePrograms again to validate the current system state.
            # Keep track of our or state
            if self.closeProgramsState == 0:
               # If we've asked the question once, bump our state.  This ensures
               # the user is now prompted to manually close the remaining UIs
               # and programs.
               self.closeProgramsState = 1
            gui.ClosePrograms(question, text, callback, default)

         if openitems:
            # Set next button to Retry
            self.SetNextType('retry', args=(None, self, self.appControl, question, text, callback, default),
                             buttonText='_Next')
            # Hijack the next callback to use for our purposes
            self._next.nextClickedAction = onSuspend
            self.EnableNext(True) # Make sure the next button is enabled.
            self.txn.ui.ResetNext() # And usable
            self._next.grab_focus()
            self.window.show()
            return
         else:
            # Set the 'Next' button to Next again, restoring old functionality
            self.SetNextType('next')
            self.EnableNext(True) # Make sure the next button is enabled.
            self.txn.ui.ResetNext() # And usable
            self._next.grab_focus()
            # Don't allow the user to go back over this question again
            self.txn.SetBackLimit(modifier=1)
            # Even if everything seems closed, we can't detect that encrypted VMs are running.
            # Pass control on to the fallback method, just to be sure.
            appControlSucceeded = True
            # In case this question is asked again, reset its state to 0.  We want
            # to start fresh.
            self.closeProgramsState = 0

      except Exception, e:
         log.info('Cannot use vmware-app-control to shut down open VMs, defaulting to fallback message.')
         log.debug('Exception: %s' % e)

      if question.checkVMsRunning():
         # If an encrypted VM is still running, or if something went wrong with App Control,
         # fall back onto our old UI
         self.ClearContents()

         space = gtk.Label()
         space.show()

         self._content.pack_start(space, True, True)
         self._content.pack_start(self._error, False)

         # Add the text prompt
         l = gtk.Label()
         l.set_alignment(0, 0.5)
         l.set_line_wrap(True)
         l.set_use_markup(True)
         if appControlSucceeded:
            l.set_markup('<b>The VMware Installer could not shut down all running virtual '
                         'machines.  If you have encrypted VMs open, please shut them down or '
                         'suspend them now and press \'Retry\' to continue.</b>')
         else:
            l.set_markup('<b>The VMware Installer cannot continue if there are running virtual '
                         'machines. Shut down or suspend running virtual machines before '
                         'continuing.</b>')
         l.show()
         self._content.pack_start(l, False)
         self._content.label = l
         def onRetry():
            if not question.checkVMsRunning():
               # Set the 'Next' button to Next again, restoring old functionality
               self.SetNextType('next')
               self.EnableNext(True) # Make sure the next button is enabled.
               self._content.label.set_markup('<b>Virtual machines have been closed.  '
                                              'Installation can continue.</b>')
            # Ensure that reset can be used again.
            self.txn.ui.ResetNext()
            self.txn.put(partial(callback, True))

         # Set next button to Retry
         self.SetNextType('retry')
         # Hijack the next callback to use for our purposes
         self._next.nextClickedAction = onRetry
         self.EnableNext(True) # Make sure the next (now Retry) button is enabled.
         self.txn.ui.ResetNext() # And usable
         self._next.grab_focus()

         space = gtk.Label()
         space.show()
         self._content.pack_end(space, True, True)
         self.window.show()
      else:
         # It's okay to move on, "click" next to move to the next panel.
         self.EnableNext(True) # Make sure the next button is enabled.
         self.txn.ui.ResetNext() # And usable
         # Don't allow the user to go back over this question again
         self.txn.SetBackLimit(modifier=1)
         self._next.clicked()


   @dispatchable
   def ShutdownProgram(self, question, text, callback, default):
      if question.Validate(None) == True:
         # The program has already been shut down.  Skip this question.
         # "click" next to move to the next panel.
         self.EnableNext(True) # Make sure the next button is enabled.
         self.txn.ui.ResetNext() # And usable
         # Don't allow the user to go back over this question again
         self.txn.SetBackLimit(modifier=1)
         self._next.clicked()
         return None

      # Otherwise ask the user to close the program
      self.ClearContents()

      space = gtk.Label()
      space.show()

      self._content.pack_start(space, True, True)
      self._content.pack_start(self._error, False)

      # 1.0 allows text box to expand entirely.
      align = gtk.Alignment(xscale=1.0, yscale=1.0)
      align.show()
      align.set_padding(0, 0, 24, 24)

      # Add the text prompt
      l = gtk.Label()
      l.set_alignment(0, 0.5)
      l.set_use_markup(True)
      l.set_markup('<b>The VMware Installer cannot continue while %s is running.  Please '
                   'shut it down and press \'Retry\'.</b>' % question.programName)
      l.set_line_wrap(True)
      l.show()
      align.add(l)
      self._content.pack_start(align, False)
      self._content.label = l

      def onRetry():
         if question.Validate(True):
            # Set the 'Next' button to Next again, restoring old functionality
            self.SetNextType('next')
            self._content.label.set_markup('<b>%s has been closed.  The VMware Installer can continue.</b>' % question.programName)
         # Ensure that the next button can be used again for the next cycle.
         self.EnableNext(True)
         self.txn.ui.ResetNext()
         self.txn.put(partial(callback, True))

      # Set next button to Retry
      self.SetNextType('retry')
      # Hijack the next callback to use for our purposes
      self._next.nextClickedAction = onRetry
      self.EnableNext(True) # Make sure the next (now Retry) button is enabled.
      self._next.grab_focus()

      space = gtk.Label()
      space.show()
      self._content.pack_end(space, True, True)
      self.window.show()


   # Mapping between question types and their functions.
   questionFunctions = { QUESTION_FILE : ShowFile,
                         QUESTION_DIRECTORY : ShowDirectory,
                         QUESTION_YESNO : ShowYesNo,
                         QUESTION_NUMERIC : ShowNumeric,
                         QUESTION_CLOSEPROGRAMS: ClosePrograms,
                         QUESTION_SHUTDOWNPROGRAM: ShutdownProgram,
                         QUESTION_TEXTENTRY: ShowTextEntry,
                         QUESTION_PORTENTRY: ShowPortEntry,
                         QUESTION_DUALPORTENTRIES: ShowDualPortEntries,}

   @dispatchable
   def ShowQuestion(self, question, text, callback, default, qtype):
      # Switch on the type.
      func = self.questionFunctions.get(qtype, None)
      if func == None:
         log.error('Attempting to show a question type that does not exist.')
         log.error('Question type = %s', type)
         log.error('Check core/questions.py for a mapping of types.')
         raise NotImplementedError

      func(self, question, text, callback, default)

   @dispatchable
   def SetErrorMessage(self, text):
      """ Set text for the error message widget """
      self._error.Show(text)

   @dispatchable
   def ShowEULA(self, text, eulaQuestion, accepted, callback):
      """Display a EULA

      text -- EULA text (str)
      eulaQuestion -- vmis.core.questions.EULA
      accepted -- bool
      callback -- XXX

      """
      def _onToggle(btn, callback, agree):
         accepted = 'yes' if agree and btn.get_active() else 'no'
         self.txn.put(partial(callback, accepted))

      self.ClearContents()

      # EULA stuffed into a gtk.ScrolledWindow
      buffer = gtk.TextBuffer()
      tag = buffer.create_tag()
      tag.set_property("family", "monospace")
      buffer.insert_with_tags(buffer.get_start_iter(), text, tag)

      eula = gtk.TextView()
      eula.set_buffer(buffer)
      eula.set_wrap_mode(gtk.WRAP_WORD)
      eula.set_editable(False)
      eula.show()

      scroll = gtk.ScrolledWindow()
      scroll.add(eula)
      scroll.set_shadow_type(gtk.SHADOW_IN)
      scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
      scroll.show()

      # EULA comment/identifier.  (When a product displays multiple EULAs,
      # this serves as a hint that each EULA displayed really is distinct.)
      caption = gtk.Label()
      caption.show()
      caption.set_justify(gtk.JUSTIFY_RIGHT)
      caption.set_markup("<span size='smaller' style='italic'>%s - "
                         "End User License Agreement</span>" %
                         escape(eulaQuestion.componentName))
      caption.set_alignment(1.0, 0)

      # Radio buttons
      agree = gtk.RadioButton(label='I _accept the terms in the license agreement.')
      agree.show()

      disagree = gtk.RadioButton(
         group=agree,
         label='I _do not accept the terms in the license agreement.')
      disagree.show()
      disagree.set_active(True)

      self._content.pack_start(scroll)
      self._content.pack_start(caption, False)
      self._content.pack_start(agree, False)
      self._content.pack_start(disagree, False)

      self._next.set_sensitive(accepted)

      if accepted:
         agree.set_active(True)

      # Connect signal last otherwise setting attributes will trigger
      # the validation callback before we want it making it flicker.
      agree.connect('toggled', _onToggle, callback, True)

      self.window.show()

   # XXX: These aren't used at the moment, but if PyGTK/Accessibility
   # plays nice in the future, we may use these again.
   @dispatchable
   def Start(self):
      """ Start the main loop """
      log.debug('Calling gtk.main()')
      self.InitialSetup()
      gtk.main()

   @dispatchable
   def Stop(self):
      """ Stop the main loop """
      log.debug('Calling gtk.main_quit()')
      gtk.main_quit()


class Question(State):
   """ Question state """
   @classmethod
   def Initialize(cls, txn, question):
      txn.ui.SetNextType('next')
      txn.ui.SetPrimaryText('Questions')
      txn.ui.EnableCancel(True)
      txn.ui.SetErrorMessage('')

      if getattr(question, 'secondaryText', None):
         txn.ui.SetSecondaryText(question.secondaryText)
      else:
         txn.ui.SetSecondaryText('Customize the installation.')

   @classmethod
   def Show(cls, txn, question):
      txn.ui.ResetNext()
      log.debug('Asking %s %s', question, question.key)

      # XXX: this has to go up in Initialize to avoid race conditions
      # with the back/next buttons.
      cls._onValidate(txn, question, question.GetDefault())

      validate = partial(cls._onValidate, txn, question)
      # XXX: Right now the installer assumes that all questions involve
      # directories.  This needs to be rethought and refactored to include
      # other types of questions.
      txn.ui.ShowQuestion(question, question.text, validate, question.GetDefault(),
                          question.type)

   @staticmethod
   def _onValidate(txn, question, answer):
      try:
         answer = question.Validate(answer)
         db.config.Set(question.component, question.key, answer)
         txn.ui.EnableNext(True)
         txn.ui.SetErrorMessage('')
      except ValidationError, e:
         log.error('%s was invalid for %s: %s', answer, question, e)
         txn.ui.SetErrorMessage(e.message)
         # Only disable if the "Next" button is "_Next"
         if txn.ui._next.get_label() == "_Next":
            txn.ui.EnableNext(False)
      except ValidationErrorNonFatal, e:
         log.error('%s was invalid for %s: %s', answer, question, e)
         txn.ui.SetErrorMessage(e.message)
         # Explicitly allow the "Next" button to be used in this case.
         if txn.ui._next.get_label() == "_Next":
            txn.ui.EnableNext(True)


class EULA(State):
   """ EULA state """
   @classmethod
   def Initialize(cls, txn, eula):
      txn.ui.SetNextType('next')
      txn.ui.SetSecondaryText(
         'Please review the following license agreement to continue.')
      txn.ui.EnableCancel(True)

   @classmethod
   def Show(cls, txn, eula):
      """
      Show the EULA to the user and get acceptance of it.

      eula is an object of type EULA from questions.py
      """
      txn.ui.ResetNext()
      accepted = False

      # XXX: this has to go up in Initialize to avoid race conditions
      # with the back/next buttons.
      try:
         eula.Validate(db.config.Get(eula.component, eula.key))
         accepted = True
      except ValidationError:
         pass

      txn.ui.ShowEULA(eula.text, eula, accepted, partial(cls._onValidate, txn, eula))

   @staticmethod
   def _onValidate(txn, eula, answer):
      try:
         answer = eula.Validate(answer)
         db.config.Set(eula.component, eula.key, answer)
         txn.ui.EnableNext(True)
      except ValidationError, e:
         log.error('%s was invalid for %s: %s', answer, eula, e)
         txn.ui.EnableNext(False)


class PromptInstall(State):
   @staticmethod
   def Initialize(txn, state):
      txn.ui.SetPrimaryText('Ready to Install')
      txn.ui.SetSecondaryText('Click Install to begin the installation process.')
      txn.ui.SetNextType('install')
      txn.ui.EnableNext(True)
      txn.ui.EnableCancel(True)

   @staticmethod
   def Show(txn, state):
      txn.ui.ResetNext()
      txn.ui.ShowPromptInstall()


class Finish(State):
   """ Finish state """
   @staticmethod
   def Initialize(txn, state):
      txn.ui.EnableBack(False)
      txn.ui.EnableCancel(False)
      txn.ui.EnableNext(True)

      txn.ui.SetNextType('close')
      txn.ui.SetPrimaryText('Finished')
      txn.ui.SetSecondaryText('The %s process is complete.' % txn.installMode.lower())

   @staticmethod
   def Show(txn, state):
      txn.ui.ResetNext()
      txn.ui.ShowFinish(txn.success, txn.message)
      txn.actions.append((Exit, None))


class Exit(State):
   @staticmethod
   def Initialize(txn, state):
      txn.ui.EnableBack(False)
      txn.ui.EnableCancel(False)
      txn.ui.EnableNext(False)

   @staticmethod
   def Show(txn, state):
      txn.ui.ResetNext()
      log.debug('Stopping transaction loop')
      txn.Quit()
